package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"runtime/debug"

	"github.com/gorilla/mux"
	"github.com/mattermost/focalboard/server/app"
	"github.com/mattermost/focalboard/server/model"
	"github.com/mattermost/focalboard/server/services/audit"
	"github.com/mattermost/focalboard/server/services/permissions"

	"github.com/mattermost/mattermost-server/v6/shared/mlog"
)

const (
	HeaderRequestedWith    = "X-Requested-With"
	HeaderRequestedWithXML = "XMLHttpRequest"
	UploadFormFileKey      = "file"
	True                   = "true"

	ErrorNoTeamCode    = 1000
	ErrorNoTeamMessage = "No team"
)

var (
	ErrHandlerPanic = errors.New("http handler panic")
)

// ----------------------------------------------------------------------------------------------------
// REST APIs

type API struct {
	app             *app.App
	authService     string
	permissions     permissions.PermissionsService
	singleUserToken string
	MattermostAuth  bool
	logger          mlog.LoggerIFace
	audit           *audit.Audit
	isPlugin        bool
}

func NewAPI(
	app *app.App,
	singleUserToken string,
	authService string,
	permissions permissions.PermissionsService,
	logger mlog.LoggerIFace,
	audit *audit.Audit,
	isPlugin bool,
) *API {
	return &API{
		app:             app,
		singleUserToken: singleUserToken,
		authService:     authService,
		permissions:     permissions,
		logger:          logger,
		audit:           audit,
		isPlugin:        isPlugin,
	}
}

func (a *API) RegisterRoutes(r *mux.Router) {
	apiv2 := r.PathPrefix("/api/v2").Subrouter()
	apiv2.Use(a.panicHandler)
	apiv2.Use(a.requireCSRFToken)

	/* ToDo:
	apiv3 := r.PathPrefix("/api/v3").Subrouter()
	apiv3.Use(a.panicHandler)
	apiv3.Use(a.requireCSRFToken)
	*/

	// V2 routes (ToDo: migrate these to V3 when ready to ship V3)
	a.registerUsersRoutes(apiv2)
	a.registerAuthRoutes(apiv2)
	a.registerMembersRoutes(apiv2)
	a.registerCategoriesRoutes(apiv2)
	a.registerSharingRoutes(apiv2)
	a.registerTeamsRoutes(apiv2)
	a.registerAchivesRoutes(apiv2)
	a.registerSubscriptionsRoutes(apiv2)
	a.registerFilesRoutes(apiv2)
	a.registerLimitsRoutes(apiv2)
	a.registerInsightsRoutes(apiv2)
	a.registerOnboardingRoutes(apiv2)
	a.registerSearchRoutes(apiv2)
	a.registerConfigRoutes(apiv2)
	a.registerBoardsAndBlocksRoutes(apiv2)
	a.registerChannelsRoutes(apiv2)
	a.registerTemplatesRoutes(apiv2)
	a.registerBoardsRoutes(apiv2)
	a.registerBlocksRoutes(apiv2)
	a.registerContentBlocksRoutes(apiv2)
	a.registerStatisticsRoutes(apiv2)
	a.registerComplianceRoutes(apiv2)

	// V3 routes
	a.registerCardsRoutes(apiv2)

	// System routes are outside the /api/v2 path
	a.registerSystemRoutes(r)
}

func (a *API) RegisterAdminRoutes(r *mux.Router) {
	r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
}

func getUserID(r *http.Request) string {
	ctx := r.Context()
	session, ok := ctx.Value(sessionContextKey).(*model.Session)
	if !ok {
		return ""
	}
	return session.UserID
}

func (a *API) panicHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if p := recover(); p != nil {
				a.logger.Error("Http handler panic",
					mlog.Any("panic", p),
					mlog.String("stack", string(debug.Stack())),
					mlog.String("uri", r.URL.Path),
				)
				a.errorResponse(w, r, ErrHandlerPanic)
			}
		}()

		next.ServeHTTP(w, r)
	})
}

func (a *API) requireCSRFToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.checkCSRFToken(r) {
			a.logger.Error("checkCSRFToken FAILED")
			a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED"))
			return
		}

		next.ServeHTTP(w, r)
	})
}

func (a *API) checkCSRFToken(r *http.Request) bool {
	token := r.Header.Get(HeaderRequestedWith)
	return token == HeaderRequestedWithXML
}

func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
	query := r.URL.Query()
	readToken := query.Get("read_token")

	if len(readToken) < 1 {
		return false
	}

	isValid, err := a.app.IsValidReadToken(boardID, readToken)
	if err != nil {
		a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err))
		return false
	}

	return isValid
}

func (a *API) userIsGuest(userID string) (bool, error) {
	if a.singleUserToken != "" {
		return false, nil
	}
	return a.app.UserIsGuest(userID)
}

// Response helpers

func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
	a.logger.Error(err.Error())
	errorResponse := model.ErrorResponse{Error: err.Error()}

	switch {
	case model.IsErrBadRequest(err):
		errorResponse.ErrorCode = http.StatusBadRequest
	case model.IsErrUnauthorized(err):
		errorResponse.ErrorCode = http.StatusUnauthorized
	case model.IsErrForbidden(err):
		errorResponse.ErrorCode = http.StatusForbidden
	case model.IsErrNotFound(err):
		errorResponse.ErrorCode = http.StatusNotFound
	case model.IsErrRequestEntityTooLarge(err):
		errorResponse.ErrorCode = http.StatusRequestEntityTooLarge
	case model.IsErrNotImplemented(err):
		errorResponse.ErrorCode = http.StatusNotImplemented
	default:
		a.logger.Error("API ERROR",
			mlog.Int("code", http.StatusInternalServerError),
			mlog.Err(err),
			mlog.String("api", r.URL.Path),
		)
		errorResponse.Error = "internal server error"
		errorResponse.ErrorCode = http.StatusInternalServerError
	}

	setResponseHeader(w, "Content-Type", "application/json")
	data, err := json.Marshal(errorResponse)
	if err != nil {
		data = []byte("{}")
	}

	w.WriteHeader(errorResponse.ErrorCode)
	_, _ = w.Write(data)
}

func stringResponse(w http.ResponseWriter, message string) {
	setResponseHeader(w, "Content-Type", "text/plain")
	_, _ = fmt.Fprint(w, message)
}

func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam
	setResponseHeader(w, "Content-Type", "application/json")
	w.WriteHeader(code)
	fmt.Fprint(w, message)
}

func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam
	setResponseHeader(w, "Content-Type", "application/json")
	w.WriteHeader(code)
	_, _ = w.Write(json)
}

func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam
	header := w.Header()
	if header == nil {
		return
	}
	header.Set(key, value)
}
